Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
94.93% covered (success)
94.93%
131 / 138
60.00% covered (warning)
60.00%
3 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApieServiceProvider
94.93% covered (success)
94.93%
131 / 138
60.00% covered (warning)
60.00%
3 / 5
31.13
0.00% covered (danger)
0.00%
0 / 1
 autoTagHashmapActions
94.74% covered (success)
94.74%
18 / 19
0.00% covered (danger)
0.00%
0 / 1
4.00
 boot
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
1 / 1
4
 register
92.41% covered (success)
92.41%
73 / 79
0.00% covered (danger)
0.00%
0 / 1
18.14
 parseConfig
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
4
 sanitizeConfig
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2namespace Apie\LaravelApie;
3
4use Apie\AiInstructor\AiInstructorServiceProvider;
5use Apie\ApieCommonPlugin\ApieCommonPluginServiceProvider;
6use Apie\ApieFileSystem\ApieFileSystemServiceProvider;
7use Apie\CmsApiDropdownOption\CmsDropdownServiceProvider;
8use Apie\Common\CommonServiceProvider;
9use Apie\Common\ContextBuilders\FrameworkContextBuilder;
10use Apie\Common\Interfaces\BoundedContextSelection;
11use Apie\Common\Interfaces\DashboardContentFactoryInterface;
12use Apie\Common\Wrappers\BoundedContextHashmapFactory;
13use Apie\Common\Wrappers\ConsoleCommandFactory as CommonConsoleCommandFactory;
14use Apie\Console\ConsoleServiceProvider;
15use Apie\Core\CoreServiceProvider;
16use Apie\Core\Session\CsrfTokenProvider;
17use Apie\DoctrineEntityConverter\DoctrineEntityConverterProvider;
18use Apie\DoctrineEntityDatalayer\Commands\ApieUpdateIdfCommand;
19use Apie\DoctrineEntityDatalayer\DoctrineEntityDatalayerServiceProvider;
20use Apie\DoctrineEntityDatalayer\EntityReindexer;
21use Apie\DoctrineEntityDatalayer\IndexStrategy\BackgroundIndexStrategy;
22use Apie\DoctrineEntityDatalayer\IndexStrategy\DirectIndexStrategy;
23use Apie\DoctrineEntityDatalayer\IndexStrategy\IndexAfterResponseIsSentStrategy;
24use Apie\DoctrineEntityDatalayer\IndexStrategy\IndexStrategyInterface;
25use Apie\Faker\FakerServiceProvider;
26use Apie\HtmlBuilders\ErrorHandler\CmsErrorRenderer;
27use Apie\HtmlBuilders\HtmlBuilderServiceProvider;
28use Apie\LaravelApie\Config\LaravelConfiguration;
29use Apie\LaravelApie\ContextBuilders\CsrfTokenContextBuilder;
30use Apie\LaravelApie\ContextBuilders\RegisterBoundedContextActionContextBuilder;
31use Apie\LaravelApie\ContextBuilders\SessionContextBuilder;
32use Apie\LaravelApie\ErrorHandler\ApieErrorRenderer;
33use Apie\LaravelApie\ErrorHandler\Handler;
34use Apie\LaravelApie\Providers\CmsServiceProvider;
35use Apie\LaravelApie\Providers\SecurityServiceProvider;
36use Apie\LaravelApie\Wrappers\Cms\DashboardContentFactory;
37use Apie\LaravelApie\Wrappers\Core\BoundedContextSelected;
38use Apie\LaravelApie\Wrappers\Queue\BackgroundProcessPersistListener;
39use Apie\Maker\MakerServiceProvider;
40use Apie\McpServer\McpServerServiceProvider;
41use Apie\RestApi\RestApiServiceProvider;
42use Apie\SchemaGenerator\SchemaGeneratorServiceProvider;
43use Apie\Serializer\SerializerServiceProvider;
44use Apie\ServiceProviderGenerator\TagMap;
45use Apie\TypescriptClientBuilder\TypescriptClientBuilderServiceProvider;
46use Illuminate\Config\Repository;
47use Illuminate\Contracts\Debug\ExceptionHandler;
48use Illuminate\Contracts\Events\Dispatcher;
49use Illuminate\Support\ServiceProvider;
50use Psr\EventDispatcher\EventDispatcherInterface;
51use Psr\Http\Message\ServerRequestInterface;
52use Symfony\Component\Config\ConfigCache;
53use Symfony\Component\Config\Definition\Processor;
54use Symfony\Component\Config\Resource\ReflectionClassResource;
55use Symfony\Component\Console\Application;
56use Symfony\Component\EventDispatcher\EventDispatcher;
57use Symfony\Component\Lock\LockFactory;
58
59class ApieServiceProvider extends ServiceProvider
60{
61    /**
62     * @var array<string, class-string<ServiceProvider>> $alreadyRegistered
63     */
64    private array $alreadyRegistered = [];
65    /**
66     * @var array<string, array<int, class-string<ServiceProvider>>> $dependencies
67     */
68    private array $dependencies = [
69        'enable_ai_instructor' => [
70            AiInstructorServiceProvider::class,
71        ],
72        'enable_common_plugin' => [
73            ApieCommonPluginServiceProvider::class,
74        ],
75        'enable_cms' => [
76            CommonServiceProvider::class,
77            HtmlBuilderServiceProvider::class, // it's important that this loads before CmsServiceProvider!!!
78            CmsServiceProvider::class,
79            SerializerServiceProvider::class,
80        ],
81        'enable_cms_dropdown' => [
82            CommonServiceProvider::class,
83            CmsDropdownServiceProvider::class,
84        ],
85        'enable_core' => [
86            CoreServiceProvider::class,
87        ],
88        'enable_console' => [
89            CommonServiceProvider::class,
90            ConsoleServiceProvider::class, // it's important that this loads after CommonServiceProvider!!!
91            SerializerServiceProvider::class,
92        ],
93        'enable_doctrine_entity_converter' => [
94            CoreServiceProvider::class,
95            DoctrineEntityConverterProvider::class,
96        ],
97        'enable_doctrine_entity_datalayer' => [
98            CoreServiceProvider::class,
99            DoctrineEntityConverterProvider::class,
100            DoctrineEntityDatalayerServiceProvider::class,
101        ],
102        'enable_security' => [
103            CommonServiceProvider::class,
104            SerializerServiceProvider::class,
105            SecurityServiceProvider::class,
106        ],
107        'enable_rest_api' => [
108            CommonServiceProvider::class,
109            RestApiServiceProvider::class,
110            SchemaGeneratorServiceProvider::class,
111            SerializerServiceProvider::class,
112        ],
113        'enable_faker' => [
114            FakerServiceProvider::class,
115        ],
116        'enable_maker' => [
117            MakerServiceProvider::class,
118        ],
119        'enable_mcp_server' => [
120            CommonServiceProvider::class,
121            SerializerServiceProvider::class,
122            McpServerServiceProvider::class,
123        ],
124        'enable_typescript_client_builder' => [
125            TypescriptClientBuilderServiceProvider::class,
126        ],
127    ];
128
129    private function autoTagHashmapActions(): void
130    {
131        $boundedContextConfig = config('apie.bounded_contexts');
132        $scanBoundedContextConfig = config('apie.scan_bounded_contexts');
133        $factory = new BoundedContextHashmapFactory(
134            $boundedContextConfig ?? [],
135            $scanBoundedContextConfig ?? [],
136            new EventDispatcher(),
137        );
138        $hashmap = $factory->create();
139        foreach ($hashmap as $boundedContext) {
140            foreach ($boundedContext->actions as $action) {
141                $class = $action->getDeclaringClass();
142                if (!$class->isInstantiable()) {
143                    continue;
144                }
145                $className = $class->name;
146                TagMap::register(
147                    $this->app,
148                    $className,
149                    ['apie.context']
150                );
151            }
152        }
153    }
154
155    public function boot(): void
156    {
157        $this->autoTagHashmapActions();
158        $this->loadViewsFrom(__DIR__ . '/../templates', 'apie');
159        $this->loadRoutesFrom(__DIR__.'/../resources/routes.php');
160        TagMap::registerEvents($this->app);
161
162        if ($this->app->runningInConsole()) {
163            $commands = [];
164            $commands[] = ApieUpdateIdfCommand::class;
165            // for some reason these are not called in integration tests without re-registering them
166            foreach (TagMap::getServiceIdsWithTag($this->app, 'console.command') as $taggedCommand) {
167                $serviceId = 'apie.console.tagged.' . $taggedCommand;
168                $this->app->singleton($serviceId, function () use ($taggedCommand) {
169                    return $this->app->get($taggedCommand);
170                });
171                $commands[] = $serviceId;
172            }
173            /** @var CommonConsoleCommandFactory $factory */
174            $factory = $this->app->get('apie.console.factory');
175            foreach ($factory->create($this->app->get(Application::class)) as $command) {
176                $serviceId = 'apie.console.registered.' . $command->getName();
177                $this->app->instance($serviceId, $command);
178                $commands[] = $serviceId;
179            }
180            $this->commands($commands);
181        }
182    }
183
184    public function register()
185    {
186        $this->mergeConfigFrom(__DIR__ . '/../resources/apie.php', 'apie');
187
188        $this->app->bind(FrameworkContextBuilder::class, function () {
189            return new FrameworkContextBuilder('laravel');
190        });
191        TagMap::register($this->app, FrameworkContextBuilder::class, ['apie.core.context_builder']);
192
193        // add PSR-14 support if needed:
194        if (!$this->app->bound(EventDispatcherInterface::class)) {
195            $this->app->bind(EventDispatcherInterface::class, function () {
196                return new class($this->app->make(Dispatcher::class)) implements EventDispatcherInterface {
197                    public function __construct(private readonly Dispatcher $dispatcher)
198                    {
199                    }
200
201                    public function dispatch(object $event): object
202                    {
203                        $this->dispatcher->dispatch($event);
204                        return $event;
205                    }
206                };
207            });
208        }
209
210        // fix for https://github.com/laravel/framework/issues/30415
211        $this->app->extend(
212            ServerRequestInterface::class,
213            function (ServerRequestInterface $psrRequest) {
214                $route = $this->app->make('request')->route();
215                if ($route) {
216                    $parameters = $route->parameters();
217                    foreach ($parameters as $key => $value) {
218                        $psrRequest = $psrRequest->withAttribute($key, $value);
219                    }
220                }
221                return $psrRequest;
222            }
223        );
224
225        $this->app->bind(IndexStrategyInterface::class, function () {
226            $config = config();
227            if ($config->get('apie.enable_doctrine_entity_datalayer')) {
228                $type = $config->get('apie.doctrine.indexing.type', 'direct');
229                return match ($type) {
230                    'direct' => new DirectIndexStrategy($this->app->get(EntityReindexer::class)),
231                    'late' => new IndexAfterResponseIsSentStrategy($this->app->get(EntityReindexer::class)),
232                    'background' => new BackgroundIndexStrategy(),
233                    default => $this->app->get(config('apie.doctrine.indexing.service', DirectIndexStrategy::class)),
234                };
235            }
236
237            return new DirectIndexStrategy($this->app->get(EntityReindexer::class));
238        });
239
240        $this->app->bind(ApieErrorRenderer::class, function () {
241            return new ApieErrorRenderer(
242                $this->app->bound(CmsErrorRenderer::class) ? $this->app->make(CmsErrorRenderer::class) : null,
243                $this->app->make(\Apie\Common\ErrorHandler\ApiErrorRenderer::class),
244                config('apie.cms.base_url')
245            );
246        });
247
248        $this->app->extend(ExceptionHandler::class, function (ExceptionHandler $service) {
249            return new Handler($this->app, $service);
250        });
251
252        $this->app->bind(LockFactory::class, function () {
253            $config = config('apie.lock_store');
254            return new LockFactory($this->app->get($config));
255        });
256        
257        $this->app->bind(DashboardContentFactoryInterface::class, DashboardContentFactory::class);
258        $this->app->bind(BoundedContextSelection::class, BoundedContextSelected::class);
259
260        $this->alreadyRegistered = [];
261        $parsedConfig = $this->parseConfig(config('apie'));
262        foreach ($this->dependencies as $configKey => $dependencies) {
263            if ($parsedConfig[$configKey] ?? false) {
264                foreach ($dependencies as $dependency) {
265                    if (!isset($this->alreadyRegistered[$dependency])) {
266                        $this->alreadyRegistered[$dependency] = $dependency;
267                        $this->app->register($dependency);
268                    }
269                }
270            }
271        }
272
273        //$this->app->bind(CsrfTokenProvider::class, CsrfTokenContextBuilder::class);
274        TagMap::register($this->app, CsrfTokenContextBuilder::class, ['apie.core.context_builder']);
275        $this->app->tag(CsrfTokenContextBuilder::class, ['apie.core.context_builder']);
276
277        // this has to be added after CsrfTokenContextBuilder!
278        $this->app->bind(SessionContextBuilder::class);
279        TagMap::register($this->app, SessionContextBuilder::class, ['apie.core.context_builder']);
280        $this->app->tag(SessionContextBuilder::class, ['apie.core.context_builder']);
281
282        TagMap::register($this->app, RegisterBoundedContextActionContextBuilder::class, ['apie.core.context_builder']);
283        $this->app->tag(RegisterBoundedContextActionContextBuilder::class, ['apie.core.context_builder']);
284        $this->app->extend('config', function (Repository $config) {
285            $this->sanitizeConfig($config);
286            $newParsedConfig = $config->get('apie');
287            foreach ($this->dependencies as $configKey => $dependencies) {
288                if ($newParsedConfig[$configKey] ?? false) {
289                    foreach ($dependencies as $dependency) {
290                        if (!isset($this->alreadyRegistered[$dependency])) {
291                            $this->alreadyRegistered[$dependency] = $dependency;
292                            $this->app->register($dependency);
293                        }
294                    }
295                }
296            }
297            return $config;
298        });
299
300        TagMap::register($this->app, BackgroundProcessPersistListener::class, ['kernel.event_subscriber']);
301    }
302
303    /**
304     * @param array<string, mixed> $rawConfig
305     * @return array<string, mixed>
306     */
307    private function parseConfig(array $rawConfig): array
308    {
309        $path = storage_path('framework/cache/apie-config' . md5(json_encode($rawConfig)) . '.php');
310        $resources = [
311            new ReflectionClassResource(new \ReflectionClass(LaravelConfiguration::class)),
312            new ReflectionClassResource(new \ReflectionClass(static::class)),
313        ];
314        $configCache = new ConfigCache($path, true);
315        if ($configCache->isFresh()) {
316            $processedConfig = require $path;
317        } else {
318            $configuration = new LaravelConfiguration();
319
320            $processor = new Processor();
321
322            $processedConfig = $processor->processConfiguration($configuration, ['apie' => $rawConfig]);
323
324            if (!isset($processedConfig['scan_bounded_contexts'])) {
325                $processedConfig['scan_bounded_contexts'] = [];
326            }
327            if (empty($processedConfig['storage'])) {
328                $processedConfig['storage'] = null;
329            }
330            $code = '<?php' . PHP_EOL . 'return ' . var_export($processedConfig, true) . ';';
331            $configCache->write($code, $resources);
332        }
333
334        return $processedConfig;
335    }
336
337    private function sanitizeConfig(Repository $config): void
338    {
339        $rawConfig = $config->get('apie');
340        $processedConfig = $this->parseConfig($rawConfig);
341
342        $config->set('apie', $processedConfig);
343    }
344}